#!/usr/bin/env python3

##===--- iwyu-mapgen-qt.py ------------------------------------------------===##
#
#                     The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.
#
##===----------------------------------------------------------------------===##

""" Generates mappings for Qt API headers.

Qt has quite a strong module convention, where there's one public header for
every module class, e.g.:

- For a module X there's typically...
- ... a set of classes called QtXy, QtXyz...
- ... and a corresponding public header for each called QtXy, QtXyz...
- ... and possibly a set of private headers called qtxy.h, qtxyz.h...

Use these conventions to generate symbol and include mappings for the entire Qt
tree.
"""

import argparse
import glob
import json
import os
import re
import sys


OUTFILEHDR = ("# Do not edit! This file was generated by the script %s." %
              os.path.basename(__file__))

QOBJECT_SYMBOLS = [
    "QObjectList",
    "qFindChildren",
    "qobject_cast",
    "QT_NO_NARROWING_CONVERSIONS_IN_CONNECT",
    "Q_CLASSINFO",
    "Q_DISABLE_COPY",
    "Q_DISABLE_COPY_MOVE",
    "Q_DISABLE_MOVE",
    "Q_EMIT",
    "Q_ENUM",
    "Q_ENUM_NS",
    "Q_FLAG",
    "Q_FLAG_NS",
    "Q_GADGET",
    "Q_INTERFACES",
    "Q_INVOKABLE",
    "Q_NAMESPACE",
    "Q_NAMESPACE_EXPORT",
    "Q_OBJECT",
    "Q_PROPERTY",
    "Q_REVISION",
    "Q_SET_OBJECT_NAME",
    "Q_SIGNAL",
    "Q_SIGNALS",
    "Q_SLOT",
    "Q_SLOTS",
    "emit",
    "slots",
    "signals",
    "SIGNAL",
    "SLOT",
]


class QtHeader(object):
    """ Carry data associated with a Qt header """
    def __init__(self, headername):
        self.headername = headername
        self.classname = os.path.basename(headername)
        self.modulename = os.path.basename(os.path.dirname(headername))
        self._private_headers = None

    def get_private_headers(self):
        """ Return a list of headernames included by this header """
        if self._private_headers is None:
            with open(self.headername, 'r') as headerfile:
                included = re.findall(r'#include "(.*)\.h"', headerfile.read())
            self._private_headers = list(included)
        return self._private_headers


def generate_imp_lines(symbols_map, includes_map):
    """ Generate json-formatted strings in .imp format.

    This should ideally return a jsonable structure instead, and use json.dump
    to write it to the output file directly. But there doesn't seem to be a
    simple way to convince Python's json library to generate a "packed"
    formatting, it always prefers to wrap dicts onto multiple lines.

    Cheat, and use json.dumps for escaping and build a string instead.
    """
    def jsonline(mapping, indent):
        return (indent * " ") + json.dumps(mapping)

    for symbol, header in symbols_map:
        map_to = "<" + header + ">"
        yield jsonline({"symbol": [symbol, "private", map_to, "public"]},
                       indent=2)

    for module, include, header in includes_map:
        # Use regex map-from to match both quoted and angled includes and
        # optional directory prefix (e.g. <QtCore/qnamespace.h> is equivalent to
        # "qnamespace.h").
        map_from = r'@["<](%s/)?%s\.h[">]' % (module, include)
        map_to = "<" + header + ">"
        yield jsonline({"include": [map_from, "private", map_to, "public"]},
                       indent=2)


def add_mapping_rules(header, symbols_map, includes_map):
    """ Add symbol and include mappings for a Qt module. """
    symbols_map += [(header.classname, header.classname)]
    for include in header.get_private_headers():
        includes_map += [(header.modulename, include, header.classname)]


def main(qtroot):
    """ Entry point. """
    symbols_map = []
    includes_map = []
    deferred_headers = []

    # Add manual overrides.
    symbols_map += [("qDebug", "QtGlobal")]
    symbols_map += [(symbol, "QObject") for symbol in QOBJECT_SYMBOLS]
    includes_map += [("QtCore", "qnamespace", "Qt")]

    # Collect mapping information from Qt directory tree.
    headers = glob.glob(os.path.join(qtroot, '**/*[!.h]'))
    for header in headers:
        if os.path.isdir(header):
            continue

        header = QtHeader(header)
        if header.classname == "QInternal":
            continue

        if header.classname == header.modulename:
            deferred_headers.append(header)
        else:
            add_mapping_rules(header, symbols_map, includes_map)

    for header in deferred_headers:
        add_mapping_rules(header, symbols_map, includes_map)

    # Print mappings
    print(OUTFILEHDR)
    print("[")
    print(",\n".join(generate_imp_lines(symbols_map, includes_map)))
    print("]")
    return 0


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("qtroot",
                        help="Qt include root (e.g. /usr/include/.../qt5)")
    args = parser.parse_args()
    sys.exit(main(args.qtroot))
